﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Security;
using System.Text;
using Microsoft.Xrm.Sdk;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Discovery;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Query;
using XrmLibrary.Context;
using XrmLibrary.Enums;
using XrmLibrary.Exceptions;
using XrmLibrary.MessageStrings;

namespace XrmLibrary.Extensions
{
    public static class OrganizationServiceExtensions
    {
        private const String EntityNameRole = "role";
        private const String EntityNameUser = "systemuser";
        private const String AttributeBusinessUnitId = "businessunitid";

        #region Retrieve Methods

        /// <summary>
        /// Performs a WhoAmI request.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <returns>WhoAmIResponse for the current user.</returns>
        public static WhoAmIResponse WhoAmI(this IOrganizationService service)
        {
            try
            {
                WhoAmIRequest request = new WhoAmIRequest();
                return (WhoAmIResponse)service.Execute(request);
            }
            catch (MessageSecurityException msEx)
            {
                throw new XrmLibraryException("An exception occurred.  The username and/or password were likely incorrect.", msEx);
            }
        }

        /// <summary>
        /// Retrieves multiple entities and internally pages through all results to compile a single EntityCollection.
        /// </summary>
        /// <param name="service">The IOrganizationService.</param>
        /// <param name="query">The QueryExpression.</param>
        /// <returns>An EntityCollection containing all retrieved entities.</returns>
        public static EntityCollection RetrieveMultipleNoLimit(this IOrganizationService service, QueryExpression query)
        {
            List<Entity> entities = new List<Entity>();
            query.PageInfo = new PagingInfo { Count = 5000, PageNumber = 1, PagingCookie = null };

            while (true)
            {
                EntityCollection results = service.RetrieveMultiple(query);

                if (results.Entities != null)
                {
                    entities.AddRange(results.Entities);
                }

                if (results.MoreRecords)
                {
                    query.PageInfo.PageNumber++;
                    query.PageInfo.PagingCookie = results.PagingCookie;
                }
                else
                {
                    break;
                }
            }
            return new EntityCollection(entities);
        }

        /// <summary>
        /// Retrieves details about a record and its attributes and formats them in a listed String format.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="recordId">The record id.</param>
        /// <returns>
        /// Record details in a String list format.
        /// </returns>
        public static String RetrieveRecordDetails(this IOrganizationService service, String entityName, Guid recordId)
        {
            ColumnSet cols = new ColumnSet { AllColumns = true };
            Entity record = service.Retrieve(entityName, recordId, cols);

            StringBuilder sb = new StringBuilder();
            sb.AppendLine(String.Format("-- {0} Details --", record.LogicalName));

            foreach (KeyValuePair<String, Object> att in record.Attributes)
            {
                RetrieveAttributeDetails(service, entityName, att);
            }
            return sb.ToString();
        }

        private static String RetrieveAttributeDetails(IOrganizationService service, String entityName, KeyValuePair<String, Object> attribute)
        {
            StringBuilder sb = new StringBuilder();
            if (attribute.Value is EntityReference)
            {
                EntityReference reference = (EntityReference)attribute.Value;
                sb.AppendTabbedKeyValueLine(attribute.Key, reference.Name);
            }
            else if (attribute.Value is OptionSetValue)
            {
                OptionSetValue optionSet = (OptionSetValue)attribute.Value;
                String optionLabel = service.RetrieveOptionSetDisplayValue(entityName, attribute.Key, optionSet.Value);
                sb.AppendTabbedKeyValueLine(attribute.Key, optionLabel);
            }
            else
            {
                sb.AppendTabbedKeyValueLine(attribute.Key, attribute.Value);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Retrieves details from entity metadata and formats it in a listed String format.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <returns></returns>
        public static String RetrieveEntityDetails(this IOrganizationService service, String entityName)
        {
            EntityMetadata entityMetadata = service.RetrieveEntityMetadata(entityName);
            return GetEntityMetadataDetails(entityMetadata);
        }

        private static EntityMetadata RetrieveEntityMetadata(this IOrganizationService service, string entityName)
        {
            RetrieveEntityRequest request = new RetrieveEntityRequest
                                                {
                                                    EntityFilters = EntityFilters.All,
                                                    LogicalName = entityName,
                                                    RetrieveAsIfPublished = false,
                                                };
            RetrieveEntityResponse response = (RetrieveEntityResponse)service.Execute(request);
            return response.EntityMetadata;
        }

        private static String GetEntityMetadataDetails(EntityMetadata entityMetadata)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("--{0} Entity Details--\n", entityMetadata.DisplayName);
            foreach (var att in entityMetadata.Attributes)
            {
                sb.AppendFormat("\tDisplayName: {0}, LogicalName: {1}\n", GetLocalLabelValue(att.DisplayName), att.LogicalName);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Gets the label value.
        /// </summary>
        /// <param name="label">The label.</param>
        /// <returns>A non-null label value.</returns>
        private static String GetLocalLabelValue(Label label)
        {
            if (label != null)
            {
                if (label.UserLocalizedLabel != null)
                {
                    if (label.UserLocalizedLabel.Label != null)
                    {
                        return label.UserLocalizedLabel.Label;
                    }
                    return label.UserLocalizedLabel.ToString();
                }
                return label.ToString();
            }
            return null;
        }

        /// <summary>
        /// Retrieves the record ids (GUIDs) that match the set of constraints.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="constraints">The constraints.</param>
        /// <returns></returns>
        public static IEnumerable<Guid> RetrieveRecordIds(this IOrganizationService service, String entityName, IEnumerable<KeyValuePair<String, Object>> constraints)
        {
            FilterExpression filter = new FilterExpression(LogicalOperator.And);
            foreach (KeyValuePair<String, Object> kvp in constraints)
            {
                filter.AddCondition(kvp.Key, ConditionOperator.Equal, kvp.Value);
            }
            QueryExpression query = new QueryExpression(entityName) { ColumnSet = new ColumnSet(), Criteria = filter };
            EntityCollection entitiesResponse = service.RetrieveMultiple(query);
            return entitiesResponse.Entities.Select(record => record.Id);
        }


        /// <summary>
        /// Retrieves the option set display value based on the specified integer value.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <param name="optionSetIntegerValue">The option set integer value.</param>
        /// <returns></returns>
        public static String RetrieveOptionSetDisplayValue(this IOrganizationService service, String entityName, String attributeName, Int32 optionSetIntegerValue)
        {
            RetrieveAttributeRequest request = new RetrieveAttributeRequest
            {
                EntityLogicalName = entityName,
                LogicalName = attributeName,
                RetrieveAsIfPublished = false
            };

            RetrieveAttributeResponse response = (RetrieveAttributeResponse)service.Execute(request);

            if (response.AttributeMetadata is EnumAttributeMetadata)
            {
                EnumAttributeMetadata enumAttribute = (EnumAttributeMetadata)response.AttributeMetadata;
                foreach (var opt in enumAttribute.OptionSet.Options)
                {
                    if (optionSetIntegerValue == opt.Value)
                    {
                        return opt.Label.UserLocalizedLabel.Label;
                    }
                }
            }

            // None found, throw exception
            throw new XrmLibraryException("No option set with specified value found.");
        }

        /// <summary>
        /// Retrieves the option set integer value based on the specified display value.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <param name="optionSetDisplayValue">The option set display value.</param>
        /// <returns></returns>
        public static Int32 RetrieveOptionSetIntegerValue(this IOrganizationService service, String entityName, String attributeName, String optionSetDisplayValue)
        {
            AttributeMetadata attributeMetadata = service.RetrieveAttributeMetadata(entityName, attributeName);
            return GetMatchingOptionSetValue(optionSetDisplayValue, attributeMetadata);
        }

        /// <summary>
        /// Retrieves attribute metadata for the specified entity and attribute.
        /// </summary>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <param name="service">The service.</param>
        /// <returns></returns>
        public static AttributeMetadata RetrieveAttributeMetadata(this IOrganizationService service, string entityName, string attributeName)
        {
            RetrieveAttributeRequest request = new RetrieveAttributeRequest
                                                   {
                                                       EntityLogicalName = entityName,
                                                       LogicalName = attributeName,
                                                       RetrieveAsIfPublished = false
                                                   };

            RetrieveAttributeResponse response = (RetrieveAttributeResponse)service.Execute(request);
            return response.AttributeMetadata;
        }

        private static int GetMatchingOptionSetValue(string optionSetDisplayValue, AttributeMetadata attributeMetadata)
        {
            optionSetDisplayValue = optionSetDisplayValue.ToUpper();
            if (attributeMetadata is EnumAttributeMetadata)
            {
                EnumAttributeMetadata enumAttribute = (EnumAttributeMetadata) attributeMetadata;
                foreach (var opt in enumAttribute.OptionSet.Options)
                {
                    if (optionSetDisplayValue == opt.Label.UserLocalizedLabel.Label.ToUpper())
                    {
                        if (opt.Value == null)
                        {
                            throw new XrmLibraryException(ErrorStrings.OptionSetDoesNotContainUniqueIntegerValue);
                        }
                        return (Int32) opt.Value;
                    }
                }
            }
            throw new XrmLibraryException(ErrorStrings.OptionSetDoesNotContainUniqueIntegerValue);
        }


        /// <summary>
        /// Retrieves the user entity from its id.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="id">The id.</param>
        /// <returns></returns>
        public static Entity RetrieveUserFromId(this IOrganizationService service, Guid id)
        {
            return service.Retrieve(EntityNameUser, id, new ColumnSet(true));
        }

        /// <summary>
        /// Retrieves the user entity with matching internal email address.  If multiple are found, the first will be returned.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="internalEmailAddress">The internal email address.</param>
        /// <returns></returns>
        public static Entity RetrieveUserFromInternalEmailAddress(this IOrganizationService service, String internalEmailAddress)
        {
            FilterExpression filter = new FilterExpression();
            filter.AddCondition(new ConditionExpression("internalemailaddress", ConditionOperator.Equal, internalEmailAddress));
            QueryExpression query = new QueryExpression(EntityNameUser)
            {
                ColumnSet = new ColumnSet(true),
                Criteria = filter
            };
            EntityCollection entities = service.RetrieveMultiple(query);
            return entities.Entities.First();
        }

        /// <summary>
        /// Retrieves the users full name from the users id (GUID).
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="id">The id.</param>
        /// <returns></returns>
        public static String RetrieveUserFullNameFromId(this IOrganizationService service, Guid id)
        {
            if (Tracer.IsDebugEnabled)
            {
                Tracer.Log(LogContext.InstantiateFatal(format:"About to retrieve calling users full name from CRM."));
            }
            const string fullNameAttribute = "fullname";
            Entity user = service.Retrieve("systemuser", id, new ColumnSet(fullNameAttribute));
            string fullName = user.GetAttributeValue<String>(fullNameAttribute);
            if (Tracer.IsDebugEnabled)
            {
                Tracer.Log(LogContext.InstantiateFatal(format:"User name '{0}' successfully retrieved from CRM.", args:fullName));
            }
            return fullName;
        }

        /// <summary>
        /// Retrieves the full name of the caller running the application.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <returns></returns>
        public static String RetrieveCallerFullName(this IOrganizationService service)
        {
            if (Tracer.IsDebugEnabled)
            {
                Tracer.Log(LogContext.InstantiateFatal(format:"About to perform WhoAmI request to CRM."));
            }
            WhoAmIRequest request = new WhoAmIRequest();
            WhoAmIResponse response = (WhoAmIResponse)service.Execute(request);
            if (Tracer.IsDebugEnabled)
            {
                Tracer.Log(LogContext.InstantiateFatal(format:"WhoAmI request successfully performed."));
            }
            return service.RetrieveUserFullNameFromId(response.UserId);
        }

        /// <summary>
        /// Retrieves the attribute value using specified entity name, attribute name, and record id.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <param name="id">The id.</param>
        /// <returns>The attribute requested.</returns>
        public static T RetrieveAttributeValue<T>(this IOrganizationService service, String entityName, String attributeName, Guid id)
        {
            Entity entity = service.Retrieve(entityName, id, new ColumnSet(attributeName));
            return (T)entity[attributeName];
        }

        /// <summary>
        /// Retrieves the attribute value for specified criteria.  If zero or multiple records are found, an XrmLibraryException will be thrown.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeNameToRetrieve">The attribute name to retrieve.</param>
        /// <param name="attributeNameToCompare">The attribute name to compare.</param>
        /// <param name="attributeValueToCompare">The attribute value to compare.</param>
        /// <returns></returns>
        public static T RetrieveAttributeValue<T>(this IOrganizationService service, String entityName, String attributeNameToRetrieve, String attributeNameToCompare, Object attributeValueToCompare)
        {
            ConditionExpression condition = new ConditionExpression(attributeNameToCompare, ConditionOperator.Equal, attributeValueToCompare);
            FilterExpression filter = new FilterExpression();
            filter.AddCondition(condition);
            QueryExpression query = new QueryExpression(entityName)
            {
                ColumnSet = new ColumnSet(attributeNameToRetrieve),
                Criteria = filter
            };
            EntityCollection entities = service.RetrieveMultiple(query);
            if (entities.Entities.Count == 0)
            {
                throw new XrmLibraryException("No records matched the RetrieveAttributeValue request.", context: new XrmLibraryExceptionContext(XrmLibraryExceptionContextReason.ZeroMatchingRecords));
            }
            if (entities.Entities.Count > 1)
            {
                throw new XrmLibraryException("Multiple records matched the RetrieveAttributeValue request.", context: new XrmLibraryExceptionContext(XrmLibraryExceptionContextReason.MultipleMatchingRecords));
            }
            return (T)entities.Entities.First()[attributeNameToRetrieve];
        }

        /// <summary>
        /// Retrieves a set of attribute values for all records that match specified criteria.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeNameToRetrieve">The attribute name to retrieve.</param>
        /// <param name="attributeNameToCompare">The attribute name to compare.</param>
        /// <param name="attributeValueToCompare">The attribute value to compare.</param>
        /// <returns></returns>
        public static IEnumerable<T> RetrieveAttributeValues<T>(this IOrganizationService service, String entityName, String attributeNameToRetrieve, String attributeNameToCompare = null, Object attributeValueToCompare = null)
        {
            QueryExpression query = new QueryExpression(entityName)
            {
                ColumnSet = new ColumnSet(attributeNameToRetrieve),
            };

            if (!String.IsNullOrEmpty(attributeNameToCompare) && attributeValueToCompare != null)
            {
                ConditionExpression condition = new ConditionExpression(attributeNameToCompare, ConditionOperator.Equal, attributeValueToCompare);
                FilterExpression filter = new FilterExpression();
                filter.AddCondition(condition);
                query.Criteria = filter;
            }

            EntityCollection entities = service.RetrieveMultiple(query);

            foreach (Entity entity in entities.Entities)
            {
                yield return (T)entity[attributeNameToRetrieve];
            }
        }

        /// <summary>
        /// Retrieves records for a specific entity with optional filter parameters for comparing an attribute with a value and ability to return a subset of columns.  The default setting is to return all columns.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeNameToCompare">The attribute name to compare (optional).</param>
        /// <param name="attributeValueToCompare">The attribute value to compare (optional).</param>
        /// <param name="columns">The columns (optional).</param>
        /// <returns></returns>
        public static IEnumerable<Entity> RetrieveRecords(this IOrganizationService service, String entityName, String attributeNameToCompare = null, Object attributeValueToCompare = null, ColumnSet columns = null)
        {
            if (columns == null)
            {
                columns = new ColumnSet(true);
            }

            QueryExpression query = new QueryExpression(entityName)
            {
                ColumnSet = columns
            };

            if (!String.IsNullOrEmpty(attributeNameToCompare) && attributeValueToCompare != null)
            {
                ConditionExpression condition = new ConditionExpression(attributeNameToCompare, ConditionOperator.Equal, attributeValueToCompare);
                FilterExpression filter = new FilterExpression();
                filter.AddCondition(condition);
                query.Criteria = filter;
            }

            EntityCollection entities = service.RetrieveMultiple(query);
            return entities.Entities;
        }

        /// <summary>
        /// Retrieves records for a specific entity with the ability to specify an array of values to match for a specific entity.  The filterOperator parameter should be specified to "AND" or "OR" these values.  The default is to return all columns.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeNameToCompare">The attribute name to compare.</param>
        /// <param name="attributeValuesToCompare">The attribute values to compare.</param>
        /// <param name="filterOperator">The filter operator.</param>
        /// <param name="columns">The columns (optional).</param>
        /// <returns></returns>
        public static IEnumerable<Entity> RetrieveRecords<T>(this IOrganizationService service, String entityName, String attributeNameToCompare, IEnumerable<T> attributeValuesToCompare, LogicalOperator filterOperator, ColumnSet columns = null)
        {
            if (columns == null)
            {
                columns = new ColumnSet(true);
            }

            QueryExpression query = new QueryExpression(entityName)
            {
                ColumnSet = columns
            };

            FilterExpression filter = new FilterExpression(filterOperator);
            foreach (T attributeValue in attributeValuesToCompare)
            {
                filter.AddCondition(new ConditionExpression(attributeNameToCompare, ConditionOperator.Equal, attributeValue));
            }
            query.Criteria = filter;

            EntityCollection entities = service.RetrieveMultiple(query);
            return entities.Entities;
        }

        /// <summary>
        /// Retrieves all records except those that match the criteria specified.  An attribute must be given as well as an array of values that must not match this attribute.  An optional filterParameter and columns may be passed in.  The default is to AND all values and to return all columns.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeNameToCompare">The attribute name to compare.</param>
        /// <param name="attributeValuesToCompare">The attribute values to compare.</param>
        /// <param name="filterOperator">The filter operator (optional).</param>
        /// <param name="columns">The columns (optional).</param>
        /// <returns></returns>
        public static IEnumerable<Entity> RetrieveRecordsExcept<T>(this IOrganizationService service, String entityName, String attributeNameToCompare, IEnumerable<T> attributeValuesToCompare, LogicalOperator filterOperator = LogicalOperator.And, ColumnSet columns = null)
        {
            if (columns == null)
            {
                columns = new ColumnSet(true);
            }

            FilterExpression filter = new FilterExpression(filterOperator);
            foreach (var attributeValue in attributeValuesToCompare)
            {
                filter.AddCondition(new ConditionExpression(attributeNameToCompare, ConditionOperator.NotEqual, attributeValue));
            }

            QueryExpression query = new QueryExpression(entityName) { ColumnSet = columns, Criteria = filter };

            EntityCollection entities = service.RetrieveMultiple(query);
            return entities.Entities;
        }

        /// <summary>
        /// Retrieves records for an entity with a many-to-many relationship that are related to a specific record ID on the related entity.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="primaryEntityName">Name of the primary entity.</param>
        /// <param name="intersectTableName">Name of the intersect table (found in the N:N relationship screen of MSCRM).</param>
        /// <param name="primaryAttributeIdName">Name of the primary entities primary attribute id.</param>
        /// <param name="relatedAttributeIdName">Name of the related entities primary attribute id.</param>
        /// <param name="relatedAttributeIdValue">The related attribute id value.</param>
        /// <param name="columns">The columns (optional).</param>
        /// <returns></returns>
        public static IEnumerable<Entity> RetrieveRecordsForManyToManyRelatedId(this IOrganizationService service, String primaryEntityName, String intersectTableName, String primaryAttributeIdName, String relatedAttributeIdName, Guid relatedAttributeIdValue, ColumnSet columns = null)
        {
            if (columns == null)
            {
                columns = new ColumnSet(true);
            }

            QueryExpression query = new QueryExpression(primaryEntityName) { ColumnSet = columns };
            LinkEntity link = new LinkEntity
            {
                LinkFromEntityName = primaryEntityName,
                LinkToEntityName = intersectTableName,
                LinkFromAttributeName = primaryAttributeIdName,
                LinkToAttributeName = primaryAttributeIdName,
                LinkCriteria = new FilterExpression(LogicalOperator.And)
                {
                    Conditions =
                    {
                        new ConditionExpression(relatedAttributeIdName, ConditionOperator.Equal, relatedAttributeIdValue)
                    }
                }
            };
            query.LinkEntities.Add(link);
            EntityCollection entities = service.RetrieveMultiple(query);
            return entities.Entities;
        }

        /// <summary>
        /// Retrieves the record count for a specific entity.  The maximum record count for a retrieve multiple is 5000 records.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <returns></returns>
        public static Int32 RetrieveRecordCount(this IOrganizationService service, String entityName)
        {
            QueryExpression query = new QueryExpression(entityName)
            {
                ColumnSet = new ColumnSet(),
            };
            EntityCollection entities = service.RetrieveMultiple(query);
            return entities.Entities.Count;
        }

        /// <summary>
        /// Retrieves the record count with a filter.  The maximum record count for a retrieve multiple is 5000 records.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeNameToCompare">The attribute name to compare.</param>
        /// <param name="attributeValueToCompare">The attribute value to compare.</param>
        /// <returns></returns>
        public static Int32 RetrieveRecordCount(this IOrganizationService service, String entityName, String attributeNameToCompare, object attributeValueToCompare)
        {
            ConditionExpression condition = new ConditionExpression(attributeNameToCompare, ConditionOperator.Equal,
                                                                    attributeValueToCompare);
            FilterExpression filter = new FilterExpression();
            filter.AddCondition(condition);
            QueryExpression query = new QueryExpression(entityName)
            {
                ColumnSet = new ColumnSet(),
                Criteria = filter
            };
            EntityCollection entities = service.RetrieveMultiple(query);
            return entities.Entities.Count;
        }

        /// <summary>
        /// Retrieves all roles related to a specific user record.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="user">The user. Must contain an Id.</param>
        /// <returns>An array of roles that the user has.</returns>
        public static IEnumerable<Entity> RetrieveAllRolesForUser(this IOrganizationService service, Entity user)
        {
            return service.RetrieveRecordsForManyToManyRelatedId(EntityNameRole, "systemuserroles", "roleid", "systemuserid", user.Id);
        }

        /// <summary>
        /// Retrieves a role record based on its name and business unit.  If the business unit is not specified, it will not be used as a filter.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="roleStr">The role STR.</param>
        /// <param name="businessUnitId">The business unit id.</param>
        /// <returns></returns>
        public static Entity RetrieveRoleByName(this IOrganizationService service, String roleStr, Guid? businessUnitId = null)
        {
            FilterExpression filter = new FilterExpression(LogicalOperator.And);
            filter.AddCondition(new ConditionExpression("name", ConditionOperator.Equal, roleStr));
            if (businessUnitId != null)
            {
                filter.AddCondition(new ConditionExpression("businessunitid", ConditionOperator.Equal, businessUnitId));
            }

            QueryExpression roleQuery = new QueryExpression
            {
                EntityName = EntityNameRole,
                ColumnSet = new ColumnSet("roleid"),
                Criteria = filter
            };

            return service.RetrieveMultiple(roleQuery).Entities[0];
        }


        /// <summary>
        /// Retrieves a role record based on specified user and role name.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="user">The user.  Business unit attribute must be populated.</param>
        /// <param name="roleName">Name of the role.</param>
        /// <returns></returns>
        public static Entity RetrieveRoleFromUser(this IOrganizationService service, Entity user, String roleName)
        {
            //TODO: Verify that the role belongs to the user.  Currently as long as role exists in business unit it will be retrieved.
            Guid targetBusinessUnitId = user.GetAttributeValue<EntityReference>(AttributeBusinessUnitId).Id;
            return service.RetrieveRoleByName(roleName, targetBusinessUnitId);
        }

        #endregion

        #region Create Methods

        /// <summary>
        /// Creates a record using the entity name and set of key value pairs.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributes">The attributes.</param>
        /// <returns>Guid of the newly created record.</returns>
        public static Guid CreateRecord(this IOrganizationService service, String entityName, IEnumerable<KeyValuePair<String, Object>> attributes)
        {
            Entity record = new Entity(entityName);
            record.Attributes.AddRange(attributes);
            Guid recordId = service.Create(record);
            return recordId;
        }

        /// <summary>
        /// Retrieves a business unit record using the specified business unit name.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="businessUnitName">Name of the business unit.</param>
        /// <returns>The entity business unit.</returns>
        public static Entity RetrieveBusinessUnitFromName(this IOrganizationService service, String businessUnitName)
        {
            IEnumerable<Entity> matchingBusinessUnits = service.RetrieveRecords("businessunit", "name", businessUnitName);
            Int32 numberOfBusinessUnits = matchingBusinessUnits.Count();
            if (numberOfBusinessUnits == 0)
            {
                throw new XrmLibraryException(String.Format("No business units were found with name '{0}'.", businessUnitName));
            }
            else if (numberOfBusinessUnits > 1)
            {
                throw new XrmLibraryException(String.Format("Multiple business units were found with name '{0}'.", businessUnitName));
            }
            return matchingBusinessUnits.First();
        }

        /// <summary>
        /// Retrieves a business unit record using the specified business unit ID (GUID).
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="businessUnitId">The business unit id.</param>
        /// <returns> The entity business unit.</returns>
        public static Entity RetrieveBusinessUnitFromId(this IOrganizationService service, Guid businessUnitId)
        {
            IEnumerable<Entity> matchingBusinessUnits = service.RetrieveRecords("businessunit", "businessunitid", businessUnitId);
            return matchingBusinessUnits.First();
        }


        /// <summary>
        /// Retrieves all the users belonging to specified team ID (GUID).
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="teamId">The team id.</param>
        /// <returns>An array of users belonging to the team.</returns>
        public static IEnumerable<Entity> RetrieveAllUsersBelongingToTeam(this IOrganizationService service, Guid teamId)
        {
            LinkEntity teamLink = new LinkEntity
            {
                LinkFromEntityName = EntityNameUser,
                LinkToEntityName = "teammembership",
                LinkFromAttributeName = "systemuserid",
                LinkToAttributeName = "systemuserid",
                LinkCriteria = new FilterExpression(LogicalOperator.And)
            };
            teamLink.LinkCriteria.AddCondition(new ConditionExpression("teamid", ConditionOperator.Equal, teamId));


            QueryExpression query = new QueryExpression(EntityNameUser) { ColumnSet = new ColumnSet(true) };
            query.LinkEntities.Add(teamLink);

            EntityCollection entities = service.RetrieveMultiple(query);
            return entities.Entities;
        }

        #endregion

        #region Update Methods

        /// <summary>
        /// Updates all entities matching constraints with a specific value for a single attribute.  Useful for updating a value across one or more records.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="service">The service.</param>
        /// <param name="entityName">Name of the entity.</param>
        /// <param name="attributeNameToUpdate">The attribute name to update.</param>
        /// <param name="attributeValue">The attribute value.</param>
        /// <param name="attributeNameToCompare">The attribute name to compare.</param>
        /// <param name="attributeValueToCompare">The attribute value to compare.</param>
        /// <returns>The number of records affected.</returns>
        public static Int32 UpdateAttributeValues(this IOrganizationService service, String entityName, String attributeNameToUpdate, Object attributeValue, String attributeNameToCompare = null, Object attributeValueToCompare = null)
        {
            QueryExpression query = new QueryExpression(entityName)
                                        {
                                            ColumnSet = new ColumnSet(attributeNameToUpdate)
                                        };

            if (!String.IsNullOrEmpty(attributeNameToCompare) && attributeValueToCompare != null)
            {
                ConditionExpression condition = new ConditionExpression(attributeNameToCompare, ConditionOperator.Equal, attributeValueToCompare);
                FilterExpression filter = new FilterExpression();
                filter.AddCondition(condition);
                query.Criteria = filter;
            }

            Int32 recordsAffected = 0;
            EntityCollection entities = service.RetrieveMultiple(query);
            foreach (Entity entity in entities.Entities)
            {
                entity[attributeNameToUpdate] = attributeValue;
                service.Update(entity);
                recordsAffected++;
            }
            return recordsAffected;
        }


        /// <summary>
        /// Sets the user or teams business unit using the specified business unit name.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="userOrTeam">The user or team.</param>
        /// <param name="businessUnitName">Name of the business unit.</param>
        public static void SetUserOrTeamsBusinessUnit(this IOrganizationService service, Entity userOrTeam, String businessUnitName)
        {
            Entity businessUnit = service.RetrieveBusinessUnitFromName(businessUnitName);
            service.SetUserOrTeamsBusinessUnit(userOrTeam, businessUnit.Id);
        }

        /// <summary>
        /// Sets the user or teams business unit using the specified business unit ID (GUID).
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="userOrTeam">The user or team.</param>
        /// <param name="businessUnitId">The business unit id.</param>
        public static void SetUserOrTeamsBusinessUnit(this IOrganizationService service, Entity userOrTeam, Guid businessUnitId)
        {
            SetBusinessSystemUserRequest request = new SetBusinessSystemUserRequest
            {
                BusinessId = businessUnitId,
                UserId = userOrTeam.Id,
                ReassignPrincipal = new EntityReference(userOrTeam.LogicalName, userOrTeam.Id)
            };
            SetBusinessSystemUserResponse response = (SetBusinessSystemUserResponse)service.Execute(request);
        }

        #endregion

        #region Associate Methods

        /// <summary>
        /// Adds a role to a specified user.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="userId">The user id.</param>
        /// <param name="role">The role.</param>
        public static void AddRoleToUser(this IOrganizationService service, Guid userId, Entity role)
        {
            EntityReferenceCollection entityReferences = new EntityReferenceCollection {role.ToEntityReference()};
            Relationship relationship = new Relationship("systemuserroles_association");
            service.Associate(EntityNameUser, userId, relationship, entityReferences);
        }

        #endregion

        #region Disassociate Methods

        /// <summary>
        /// Removes a role from a specified user.
        /// </summary>
        /// <param name="service">The service.</param>
        /// <param name="userId">The user id.</param>
        /// <param name="role">The role.</param>
        public static void RemoveRoleFromUser(this IOrganizationService service, Guid userId, Entity role)
        {
            EntityReferenceCollection entityReferences = new EntityReferenceCollection {role.ToEntityReference()};
            Relationship relationship = new Relationship("systemuserroles_association");
            service.Disassociate(EntityNameUser, userId, relationship, entityReferences);
        }

        #endregion

        //TODO: Finish this!!!
        /*public static IEnumerable<Entity> RetrieveSystemAdministrators(this IOrganizationService service)
        {
            FilterExpression filterExpression = new FilterExpression(LogicalOperator.And);
            QueryExpression query = new QueryExpression(EntityNameUser);
            LinkEntity linkEntity = new LinkEntity(EntityNameUser, EntityNameRole, "systemuserid", );
            query.LinkEntities.Add
            
        }*/
    }
}

